#include <EEPROM.h>
#include <math.h>

// === Hardware configuration ===

#define DEBUG 1

#define PIN_RED    9
#define PIN_GREEN  10
#define PIN_BLUE   11
#define PIN_MIC    0


// === Global constants and variables ===

/* The actual colors are defined by a two-step process.
   First, their general values are coarsely defined below.
   Then linear interpolation is used by the program to calculate
   the values in between, preventing abrupt color changes.
   
   In this way you can define a really complicated precalculated
   color mapping, while still being fast and conservative with memory.
   Engine Control Units use lookup tables in a similar way.
   
   Note: all arrays must have same size! */
/*byte CoarseHue[] = { 130, 128, 131, 132, 132, 133 };
byte CoarseSat[] = { 240, 240, 163, 160, 162, 161 };
byte CoarseLum[] = {  96, 100, 149, 184, 200, 214 };*/

byte CoarseR[] =   { 0, 0, 92, 155, 183, 209 };
byte CoarseG[] =   { 153, 168, 188, 211, 223, 234 };
byte CoarseB[] =   { 204, 213, 224, 235, 241, 246 };


const byte dim_curve[]  = {
    0,   1,   1,   2,   2,   2,   2,   2,   2,   3,   3,   3,   3,   3,   3,   3,
    3,   3,   3,   3,   3,   3,   3,   4,   4,   4,   4,   4,   4,   4,   4,   4,
    4,   4,   4,   5,   5,   5,   5,   5,   5,   5,   5,   5,   5,   6,   6,   6,
    6,   6,   6,   6,   6,   7,   7,   7,   7,   7,   7,   7,   8,   8,   8,   8,
    8,   8,   9,   9,   9,   9,   9,   9,   10,  10,  10,  10,  10,  11,  11,  11,
    11,  11,  12,  12,  12,  12,  12,  13,  13,  13,  13,  14,  14,  14,  14,  15,
    15,  15,  16,  16,  16,  16,  17,  17,  17,  18,  18,  18,  19,  19,  19,  20,
    20,  20,  21,  21,  22,  22,  22,  23,  23,  24,  24,  25,  25,  25,  26,  26,
    27,  27,  28,  28,  29,  29,  30,  30,  31,  32,  32,  33,  33,  34,  35,  35,
    36,  36,  37,  38,  38,  39,  40,  40,  41,  42,  43,  43,  44,  45,  46,  47,
    48,  48,  49,  50,  51,  52,  53,  54,  55,  56,  57,  58,  59,  60,  61,  62,
    63,  64,  65,  66,  68,  69,  70,  71,  73,  74,  75,  76,  78,  79,  81,  82,
    83,  85,  86,  88,  90,  91,  93,  94,  96,  98,  99,  101, 103, 105, 107, 109,
    110, 112, 114, 116, 118, 121, 123, 125, 127, 129, 132, 134, 136, 139, 141, 144,
    146, 149, 151, 154, 157, 159, 162, 165, 168, 171, 174, 177, 180, 183, 186, 190,
    193, 196, 200, 203, 207, 211, 214, 218, 222, 226, 230, 234, 238, 242, 248, 255
};

/* Number of subdivisions in the "fine scale" for interpolation between two coarse values.
   Ensure there's a bit of headroom left for overflows (i.e. don't approach sizeof(int) = 32767). */
const int NBR_FINE_DIVISIONS = 10000;

/* Amount moving up the fine scale when detecting sound.
   This value will be multiplied by the actual volume as measured with analogRead,
   which is in the range 0 .. 1023 */
int MIC_BASELINE;

// Amount moving down the fine scale each iteration
int SCALE;

// Milliseconds between each iteration
int DELAY_MS; 

// Index into the coarse values; range is 0 .. sizeof(CoarseHue) - 1
int CoarseIndex = 0;

/* Current fine level used by the interpolation routine; range is 0 .. NBR_FINE_DIVISIONS - 1
   It represents the fraction of "in-between-ness" between the current coarse
   value and the next one. */
int FineLevel = 0;

// === Setup and main loop ===

void setup()
{
    // Configure hardware
    pinMode(PIN_RED, OUTPUT);   
    pinMode(PIN_GREEN, OUTPUT);
    pinMode(PIN_BLUE, OUTPUT);
    pinMode(PIN_MIC, INPUT);
    
    // Load settings
    LoadInt(&MIC_BASELINE, 0, 10);
    LoadInt(&SCALE, 2, 1000);
    LoadInt(&DELAY_MS, 4, 100);
    
    // Prepare communication and show menu
    Serial.begin(9600);
    printMenu();
}


void loop()
{
    checkMenu();
    
    int micLevel = analogRead(PIN_MIC);
    // Sound makes FineLevel move forward, but silence causes it to come back
    FineLevel += SCALE * (micLevel - MIC_BASELINE);
   
    // If FineLevel overflowed (> NBR_FINE_DIVISIONS), move CoarseIndex up and renormalize FineLevel
    for (; FineLevel > NBR_FINE_DIVISIONS; FineLevel -= NBR_FINE_DIVISIONS)
        ++CoarseIndex;
    
    // If FineLevel underflows (< 0), move CoarseIndex down and renormalize FineLevel
    for (; FineLevel < 0; FineLevel += NBR_FINE_DIVISIONS)
        --CoarseIndex;
    
    // Above code has constrained FineLevel to be in range 0 .. NBR_FINE_DIVISIONS - 1
    // Constrain CoarseIndex too to prevent going out-of-bounds
    int maxIndex = sizeof(CoarseR) - 1;
    if (CoarseIndex > maxIndex)
    {
        // Don't use the last index itself; this simplifies the interpolation below
       CoarseIndex = maxIndex - 1;
       FineLevel = NBR_FINE_DIVISIONS - 1;
    } 
    else if (CoarseIndex < 0) 
    {
       CoarseIndex = 0;
       FineLevel = 0;
    }      
    
    // Calculate the appropriate interpolated hue, saturation and luminance
    float fractionalFineLevel = ((float) FineLevel) / NBR_FINE_DIVISIONS; // 0.0 .. 1.0
    /*
    int delta = CoarseHue[CoarseIndex + 1] - CoarseHue[CoarseIndex];
    float nextHue = CoarseHue[CoarseIndex] + (float) (fractionalFineLevel * delta);
    
    delta = CoarseSat[CoarseIndex + 1] - CoarseSat[CoarseIndex];
    float nextSat = CoarseSat[CoarseIndex] + (float) (fractionalFineLevel * delta);
    
    delta = CoarseLum[CoarseIndex + 1] - CoarseLum[CoarseIndex];
    float nextLum = CoarseLum[FineLevel] + (float) (fractionalFineLevel * delta);
    */
    
    int delta = CoarseR[CoarseIndex + 1] - CoarseR[CoarseIndex];
    float nextR = CoarseR[CoarseIndex] + (float) (fractionalFineLevel * delta);
    
    delta = CoarseG[CoarseIndex + 1] - CoarseG[CoarseIndex];
    float nextG = CoarseG[CoarseIndex] + (float) (fractionalFineLevel * delta);
    
    delta = CoarseB[CoarseIndex + 1] - CoarseB[CoarseIndex];
    float nextB = CoarseB[FineLevel] + (float) (fractionalFineLevel * delta);
    
    setOutput(nextR, nextG, nextB);
    
    
    #ifdef DEBUG
  /*Serial.print("micLevel: ");
    Serial.println(micLevel);*/
    Serial.print("CoarseIndex: ");
    Serial.println(CoarseIndex);
    Serial.print("FineLevel: ");
    Serial.println(FineLevel);
    Serial.print("fractionalFineLevel: ");
    Serial.println(fractionalFineLevel);
    
/*    Serial.print("nextHue: ");
    Serial.println(nextHue);
    Serial.print("nexSat: ");
    Serial.println(nextSat);
    Serial.print("nextLum: ");
    Serial.println(nextLum);*/
    
    Serial.print("nextR: ");
    Serial.println(nextR);
    Serial.print("nextG: ");
    Serial.println(nextG);
    Serial.print("nextB: ");
    Serial.println(nextB);
    #endif
    
    // Convert hue-sat-lum to RGB
//    SetLEDsFromHSL(nextHue, nextSat, nextLum);
    // Finally, time to rest, until the next cycle at least! :-)
    delay(DELAY_MS);
}


void SetLEDsFromHSL(float hue, float sat, float lum)
{
    float r = 0.0F, g = 0.0F, b = 0.0F;

    lum = dim_curve[(byte)(lum * 255)] / 256.0F;
    sat = 1.0 - (dim_curve[(byte)((1.0F - sat) * 255)] / 256.0F);

    float C = lum * sat;
    float Hx = hue / (M_PI / 3.0F);
    float X = C * (1 - fabs((fmod(Hx, 2.0F)) - 1));

    if (0 <= Hx && Hx < 1) { r = C; g = X; b = 0; }
    else if (1 <= Hx && Hx < 2) { r = X; g = C; b = 0; }
    else if (2 <= Hx && Hx < 3) { r = 0; g = C; b = X; }
    else if (3 <= Hx && Hx < 4) { r = 0; g = X; b = C; }
    else if (4 <= Hx && Hx < 5) { r = X; g = 0; b = C; }
    else if (5 <= Hx && Hx < 6) { r = C; g = 0; b = X; }

    float m = lum - C;
    
    // Drive the LED strip
   /* Serial.print("for red: ");
    Serial.println(r+m);
    Serial.print("for green: ");
    Serial.println(g+m);
    Serial.print("for blue: ");
    Serial.println(b+m);*/
    
    setOutput((256 * (r + m)), (256 * (g + m)), (256 * (b + m)));
}

void setOutput(int red, int green, int blue) {
    #ifdef DEBUG
    Serial.println("Red " + String(red) + " Green " + String(green) + " Blue " + String(blue));
    #endif
    analogWrite(PIN_RED, red);
    analogWrite(PIN_GREEN, green);
    analogWrite(PIN_BLUE, blue);
   // Serial.println(red + " " + green + " " + blue);
}

void printMenu()
{
    Serial.println("\n--- Interactive Logo ---");
    
    Serial.print("C : calibrate to current mic level; is ");
    Serial.println(MIC_BASELINE);
    
    Serial.print("F# : set scaling factor to #; is ");
    Serial.println(SCALE);
    
    Serial.print("T# : set time delay to #; is ");
    Serial.println(DELAY_MS);

    Serial.println("S : save settings to EEPROM\n");
}


void checkMenu()
{
    if (Serial.available() > 0)
    {
        /* Read first character, the menu option. There's a little trick
         * to force letters into uppercase by clearing bit 5 */
        int option = Serial.read() & ~0x20;
        
        if (option == 'S')
            SaveSettings();
        else if (option == 'C')
            MIC_BASELINE = analogRead(PIN_MIC);
        else
        {
            int result = readInt();
            switch (option)
            {
                case 'F': SCALE = result; break;
                case 'T': DELAY_MS = result; break;
            }
        }
		
		printMenu();
    }
}

void turnOff() 
{
   setOutput(0, 0, 0); 
   Serial.println("Output off\n"); 
}

int readInt()
{
    int result = 0;
    while (Serial.available() > 0)
    {
        int ch = Serial.read();
        if (ch >= '0' && ch <= '9')
            result = (result * 10) + (ch - '0'); // Add digit
    }
    return result;
}

void LoadInt(int* destination, int address, int defaultValue)
{
    *destination = EEPROM.read(address++) << 8;
    *destination |= EEPROM.read(address);
    if (*destination == (int)(0xFFFF))
        *destination = defaultValue;
}

void SaveSettings()
{
    int a = 0;
    EEPROM.write(a++, highByte(MIC_BASELINE));
    EEPROM.write(a++, lowByte(MIC_BASELINE));
    EEPROM.write(a++, highByte(SCALE));
    EEPROM.write(a++, lowByte(SCALE));
    EEPROM.write(a++, highByte(DELAY_MS));
    EEPROM.write(a++, lowByte(DELAY_MS));
    Serial.println("Saved.");
}

